package javango.contrib.hibernate;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;

import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;

import javango.db.ManagerException;
import javango.db.QuerySet;

public class HibernateQuerySet<T> implements QuerySet<T>, Iterable<T> {

	Map<String, Object> valueFilters = new LinkedHashMap<String, Object>();
	Map<String, String> propertyFilters = new LinkedHashMap<String, String>();
	List<String> orderbys = new ArrayList<String>(); 
	
	Integer first;
	Integer last;
	
	// cached local copy of my criteria,  as I am immutable this can be cached once created
	private Criteria _criteria;
	
	HibernateQuerySetHelper qsHelper;

	
	// cached local copy of my count,  as I am immutable this can be cached once known.
	// subsequent calls to count() will always return the same value even if the underlying db changes.
	// i guess this could cause strangeness if the db was updated.  oh well DDN
	Long _count;
	
	
	HibernateUtil hibernateUtil;
	Class clazz;
	
	// Create a new instance that is a clone of the other instance.
	private HibernateQuerySet(HibernateQuerySet<T> other) {
		this.valueFilters.putAll(other.valueFilters);
		this.propertyFilters.putAll(other.propertyFilters);
		this.orderbys.addAll(other.orderbys);
		this.first = other.first;
		this.last = other.last;
		this.clazz = other.clazz;
		this.hibernateUtil = other.hibernateUtil;
	}
	
	@AssistedInject
	public HibernateQuerySet(HibernateUtil hibernateUtil, @Assisted Class clazz) throws ManagerException {
		super();
		this.hibernateUtil = hibernateUtil;
		this.clazz = clazz;
	}

	public QuerySet<T> filter(Map<String, Object> params) throws ManagerException {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
		// ignore null values,  this will change in .34
		// other.filters.putAll(params);
		for(Entry<String, Object> entry : params.entrySet()) {
			if (entry.getValue() != null) other.valueFilters.put(entry.getKey(), entry.getValue());
		}
		return other;
	}
	
	public QuerySet<T> filter(Object params) throws ManagerException {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);

		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);				
				if (value != null) other.valueFilters.put(fieldName, value);
			}
		} catch (IllegalAccessException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (InvocationTargetException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (NoSuchMethodException e) {
			LogFactory.getLog(HibernateQuerySet.class).error(e,e);
			throw new ManagerException(e);
		}	
		return other;
	}

	public QuerySet<T> filter(String property, Object value) throws ManagerException {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
		other.valueFilters.put(property, value);
		return other;
	}

	public QuerySet<T> filterByProperty(String propertyA, String propertyB) throws ManagerException {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
		other.propertyFilters.put(propertyA, propertyB);
		return other;
	}
	
	public QuerySet<T> limit(int first, int last) throws ManagerException {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
		other.first = first;
		other.last = last;
		return other;
	}
	
	public T get() throws ManagerException {
		try {
			return (T)getCriteria().uniqueResult();
		} catch (HibernateException e) {
			throw new ManagerException(e);
		}
	}

	public List<T> list() throws ManagerException {
		return getCriteria().list();
	}
	
	public Iterator<T> iterator() {
		try {
			return list().iterator();
		} catch (ManagerException e) {
			// TODO What to do with this exception
			throw new RuntimeException(e);
		}
	}

	public QuerySet<T> orderBy(String... orderbys) {
		HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
		for(String order : orderbys) {
			other.orderbys.add(order);
		}
		return other;
	}

//	public QuerySet<T> or(QuerySet<T> _orOther) throws ManagerException {
//		if (_orOther instanceof HibernateQuerySet) {
//			HibernateQuerySet<T> orOther = (HibernateQuerySet<T>)_orOther;
//			if (orOther.clazz != this.clazz) {
//				throw new ManagerException("ORs only supported for the same model");
//			}
//			HibernateQuerySet<T> other = new HibernateQuerySet<T>(this);
//			other.ors = new ArrayList<HibernateQuerySet>();
//			other.ors.add(this);
//			other.ors.add(orOther);
//			return other;
//		}
//		throw new ManagerException("ORs only supported with HibernateQuerySets");
//	}
	
	// impelemtation specific functions	
	public long count() throws ManagerException {
		if (_count == null) {
			try {
				_count = new Long(hibernateUtil.doCount(getRawCriteria()));
			} catch (ManagerException e) {
				throw new ManagerException(e);
			}
		}
		return _count;
	}

	/**
	 * Returns a Criteria prior to any limit or orders being applied.
	 * 
	 * @return
	 * @throws ManagerException
	 */
	protected Criteria getRawCriteria() throws ManagerException {
		Criteria rawCriteria = hibernateUtil.getSession().createCriteria(clazz);
		qsHelper = new HibernateQuerySetHelper(hibernateUtil, clazz, rawCriteria);
		qsHelper.updateCriteria(valueFilters);
		qsHelper.updateCriteriaFromPropertyMap(propertyFilters);
		return rawCriteria;
	}
	
	/**
	 * Returns a hibernate criteria representing this queryset.
	 * 
	 * @return
	 */
	public org.hibernate.Criteria getCriteria() throws ManagerException {
		if (_criteria != null) return _criteria;

		_criteria = getRawCriteria();
		
		if (this.first != null) {
			_criteria.setFirstResult(first < 1 ? 0 : first - 1);
			if (this.last != null) {
				_criteria.setMaxResults(this.last >= this.first ? this.last-this.first+1 : 1); 
			}
		} else if (this.last != null) {
			_criteria.setMaxResults(this.last > 0 ? this.last : 1); // zero seems to cause hibernate problems in the sql
		}
		for (String s : orderbys) {
			qsHelper.addOrderBy(s);
		}
		
		return _criteria;
	}

}
